package com.yvan.serverless.script;

import com.google.common.collect.Maps;
import com.yvan.YvanUtil;
import com.yvan.serverless.GraalApplication;
import com.yvan.serverless.script.internal.GraalInterop;
import com.yvan.serverless.script.internal.GraalLoggerFactory;
import com.yvan.serverless.script.internal.LoggerConsole;
import com.yvan.serverless.script.internal.support.GraalObjectToString;
import com.yvan.serverless.utils.FileUtilExt;
import com.yvan.serverless.utils.ScriptUtils;
import lombok.SneakyThrows;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 脚本上下文
 */
public class ScriptContext {
    private static final Logger log = LoggerFactory.getLogger(ScriptContext.class);

    private final ScriptContextHolder holder;
    private final Context context;
    private final Engine engine;
    //    private final Value serverApi;
//    private final Value serviceBeans;
    private final AtomicInteger executeCount = new AtomicInteger(0);
    private final AtomicBoolean isExecuting = new AtomicBoolean(false);
    // private static volatile Semaphore lock = new Semaphore(1);
    private final AtomicLong executeCost = new AtomicLong(0L);
    private final boolean executeForceClear;

    /**
     * 反复重试关闭
     */
    public void close() {
        this.context.close(true);
    }

    public Engine getEngine() {
        return engine;
    }

    @SneakyThrows
    private void lock() {
        //lock.acquire(1);
        context.enter();
        executeCount.incrementAndGet();
        isExecuting.set(true);
    }

    private void unlock() {
        isExecuting.set(false);
        context.leave();
        //lock.release(1);
    }

    /**
     * 初始化 serverLess 脚本上下文
     */
    public ScriptContext(ScriptContextHolder holder, boolean executeForceClear) {
        engine = holder.engine;
        String distLocation = holder.distLocation;
        this.executeForceClear = executeForceClear;
        this.holder = holder;

        Context.Builder builder = Context.newBuilder("js")
                .allowIO(true)
                .allowAllAccess(true)
                .engine(engine)
                .allowExperimentalOptions(true)
                .option("js.nashorn-compat", "true")
                .option("js.ecmascript-version", "5");

        String srcPath = FileUtilExt.normalizePath(distLocation);
        if (srcPath.length() > 2 && srcPath.charAt(2) == ':') {
            srcPath = srcPath.substring(1);
        }
        srcPath = srcPath + "/";
        log.info("== Create ScriptContext path:{}", srcPath);

        builder.option("js.commonjs-require", "true")
                //.option("js.commonjs-global-properties", "../globals.js")
                .option("js.commonjs-require-cwd", srcPath);

        this.context = builder.build();

        Value engineBindings = this.context.getBindings("js");

        LoggerConsole.Instance.setObjectToString(GraalObjectToString.Instance);
        engineBindings.putMember("console", LoggerConsole.Instance);
        engineBindings.putMember("print", LoggerConsole.Instance);
        engineBindings.putMember("LoggerFactory", GraalLoggerFactory.Instance);
        engineBindings.putMember("Interop", GraalInterop.Instance);
        engineBindings.putMember("Application", GraalApplication.Instance);
    }


    /**
     * 客户端请求，生成调用的代理
     */
    public String createBroker(String url, String serverPrefix) {
        long startTimeMillis = System.currentTimeMillis();
        try {
            this.lock();

            if (url.toLowerCase().endsWith(".js")) {
                url = url.substring(0, url.length() - ".js".length());
            }

            String invokeUrl = url;
            // invokeUrl = '/!/sys/project/ProjectService'
            if (!invokeUrl.toLowerCase().startsWith(serverPrefix.toLowerCase())) {
                invokeUrl = serverPrefix + invokeUrl;
            }

            Value moduleInstance = getServiceInstance(url.toLowerCase().startsWith(".") ? url : "." + url);

            StringBuilder result = new StringBuilder();
            result.append("define(function (require, exports) {\n");
            result.append("  exports.funcs = ");
            result.append(YvanUtil.toJson(moduleInstance.getMemberKeys())).append(";\n");

            result.append("  exports.invokeUrl = '").append(invokeUrl).append("';\n");
            for (var methodName : moduleInstance.getMemberKeys()) {
                // "Symbol.keyFor(dataSource.func)"
                result.append("  exports." + methodName + " = Symbol.for('" + url + "@" + methodName + "');\n");
            }
            result.append("})");
            return result.toString();

        } finally {
            executeCost.addAndGet(System.currentTimeMillis() - startTimeMillis);
            this.unlock();
        }
    }

    /**
     * 执行任意方法
     *
     * @param path       ts 文件路径
     * @param methodName 方法名
     * @param argsRaw    参数
     * @return 执行结果
     */
    public Object executeMethod(String path, String methodName, Object... argsRaw) {
        long startTimeMillis = System.currentTimeMillis();
        try {
            this.lock();
            var exports = getServiceInstance(path);
            Object obj;
            if (argsRaw != null) {
                obj = exports.invokeMember(methodName, argsRaw);
            } else {
                obj = exports.invokeMember(methodName);
            }
            return ScriptUtils.toValue(obj);

        } finally {
            executeCost.addAndGet(System.currentTimeMillis() - startTimeMillis);
            if (holder.inspectEnable && this.executeForceClear) {
                // 刷新状态，实时清空
                holder.clearCurrentScriptContext();
            }
            this.unlock();
        }
    }

    /**
     * 获取某个服务模块的实例
     */
    private Value getServiceInstance(String path) {
        return ScriptUtils.require(this.context, path);

//        Value serviceInstance = serviceClass.getMember("prototype");
//        if (serviceInstance == null || serviceInstance.isNull() || !serviceInstance.hasMember("_instance")) {
//            throw new RuntimeException("can't get moduleInstance: " + path);
//        }
//        return serviceInstance.getMember("_instance");
    }

//    /**
//     * 清理 defer 函数
//     */
//    private void runAndClearDefer() {
//        this.serverApi.invokeMember("_runAndClearDefer");
//    }

    public Map<String, Object> getState() {
        Map<String, Object> ret = Maps.newLinkedHashMap();
        context.resetLimits();
        ret.put("executeCount", executeCount.get());
        ret.put("isExecuting", isExecuting.get());
        ret.put("executeCost", executeCost.get());
        return ret;
    }
}
